#Copyright (c) 2012-8 Civa Lin. All rights reserved.
"""
Slots Utility 空欄位工具模組

本模組提供空欄位的獲取功能，比方說尋找沒被佔用的 id 名、沒被佔用的檔名等。
使用者可自行撰寫插件函式，來自定義空欄位的產生方式。
"""

import os.path
import functools

from . import putil

class slotfunc:
    '''儲放 slotfunc 的地方

        slotfunc 的基本格式為：
            func(name: str, restrict: set) -> str

            除此之外，使用者可另加任意其他參數來解決問題
        '''
    __slots__ = ()

    def filenum(name: str, restrict: set, index: int = 0) -> str:
        '''計算可用檔案欄位的 slotfunc

            name     = 檔名（可含路徑）
            restrict = 禁用檔名集合
                       注意：
                           本函式假設 restrict 中的所有路徑，均以
                           civa.putil.norm() 處理後的正規格式存在。
            restrict = 可用檔名

            如果 name 代表的檔案位置已被佔用，或受 restrict 限制而不可用，
            則會在主檔名後加下底線與數字來擴展名稱。
            '''
        # 測試路徑為 norm_path，通過測試時的回傳為 path
        if index == 0:
            path = name
        else:
            base = putil.baseof(name) # 主檔名
            path = putil.chbase(name, base + '_' + str(index))
        norm_path = putil.norm(path)
        # 資料準備妥當，開始判斷遞歸
        if norm_path in restrict or os.path.lexists(path):
            return slotfunc.filenum(
                    name = name, restrict = restrict, index = index + 1)
        else:
            return norm_path

    def num(name: str, restrict: set, index: int = 0) -> str:
        '''透過在 name 後面加入底線與數字來擴展名稱'''
        if index == 0:
            test_name = name
        else:
            test_name = name + '_' + str(index)
        # 遞規條件：測試名不存在於限制集之中
        if test_name in restrict:
            return slotfunc.num(
                    name = name, restrict = restrict, index = index + 1)
        else:
            return test_name

def slotof(names, *, slotfunc = slotfunc.num, restrict: set = set()):
    '''依據期望名字（可為字串或列表），產生可用名字（字串或列表）。

        撰寫不同的 slotfunc 可以有不同的可用名推導方式，或是引入
        額外的限制條件，
        舉例來說：像是取得尚未被佔用的檔名。

        names    = 想要哪些名稱。其值可為 list, tuple 或 str
        slotfunc = 一個可呼叫物體（如函式），用來產生單一的可用名稱。
                   該函式格式為
                    slotfunc(name: str, restrict: set) -> str
                        name     = 為期望的名稱字串。
                        restrict = 為禁用名稱集合。
                        return ==> 實際可用的名稱字串。
        restrict = 一個包括禁用名稱的 set 或 frozenset

        return ==> 可用名稱
                   如輸入為列表則回傳也為列表，且列表結構與輸入結構一致。
        '''
    def _slotof(names, restrict, slotfunc):
        '''slotof() 的內部實作（去掉檢查與初始化）'''
        # 遞歸條件：是有序容器物件
        if isinstance(names, (list, tuple)):
            _partial_self = functools.partial(
                    _slotof, restrict = restrict, slotfunc = slotfunc)
            return list(map(_partial_self, names))
        # 終點條件：是字串
        elif isinstance(names, str):
            unused_name = slotfunc(name = names, restrict = restrict)
            restrict.add(unused_name) # 更新限制集
            return unused_name
        else: raise TypeError('"names" must a str, list or tuple')
    # 檢查類型正確
    if not callable(slotfunc):
        raise TypeError('"slotfunc" must be callable!')
    if not isinstance(names, (list, tuple, str)):
        raise TypeError('"names" must a str, list or tuple!')
    if not isinstance(restrict, (set, frozenset)):
        raise TypeError('"restrict" must a set or frozenset!')
    # 淺複製 restrict 以避免汙染外界
    return _slotof(names, set(restrict), slotfunc)

def slotof_file(filenames, *, restrict = set()):
    '''輸入期望使用的檔案名稱（含路徑），回傳「未被佔用的檔名（含路徑）」。

        如果一次傳入多個檔名，回傳的檔名之間，也會彼此互相避開。
        如：
            slotof(['test.txt','test.txt'])
            "test.txt" 路徑能用 ==>
                ['test.txt', 'test_1.txt']
            "test.txt" 路徑，已被其他檔案或資料夾佔據 ==>
                ['test_1.txt', 'test_2.txt']

        filenames = 期望的本地檔案路徑，或是一個含檔案路徑的列表
                    （列表可多層遞歸）
        restrict  = 集合或單一路徑字串，包含強制不可使用的檔案路徑。

        return  ==> 如果 filename 為檔名，回傳可用的檔名。
                    若為 filename 為列表，依相應順序與結構，回傳
                    可用檔名的列表。
                    '''
    norm_restrict = set(map(putil.norm, restrict))
    return slotof(
            names = filenames,
            slotfunc = slotfunc.filenum,
            restrict = norm_restrict)

